Skip to content

test(uds): wait on the request lock instead of sleeping - #186

Merged
dborgards merged 2 commits into
mainfrom
claude/pensive-wozniak-ivbwba
Sep 27, 2026
Merged

dborgards merged 2 commits into
mainfrom
claude/pensive-wozniak-ivbwba

Conversation

@dborgards

Copy link
Copy Markdown
Owner

What does this change?

This is the second step of #171. It covers the UDS lock and queue rows from item 7 of the delay/sleep audit.

Several UDS tests slept for a fixed time and hoped another call had reached _requestLock in the meantime. They now wait on internal observables:

  • RequestLockContended: a call found the lock already held.
  • RequestLockAcquired: a call has taken the lock.

Both events are raised from a new AcquireRequestLockAsync helper and have no subscriber in production.

Converted tests (the commit message lists them):

  • SecurityAccess_Holds_Lock_Across_Seed_And_Key now requires a keep-alive tick to have actually queued behind SecurityAccess. Its old form counted 30 ms periods and could pass with no tick at all.
  • DownloadAsync_Holds_Exclusive_Lock_Against_Concurrent_TesterPresent requires both TesterPresent calls to be seen queued on the lock while the download holds it. This replaces the 2 ms race width, and the test asserts that the queuing happened.

Mutation checks run locally:

  • Taking the lock without the caller's token makes An_Already_Cancelled_Call_Does_Not_Take_A_Free_Request_Lock go red.
  • Always disposing the semaphore in Dispose makes N_Dispose_Leaves_The_Lock_To_A_Holder_That_Outlasts_The_Wait go red, in 3 of 3 runs.

Deliberately not in this PR: the P2/P2 time seam.* A clock seam in UdsClientImpl (virtual arrival stamps) was prepared and then dropped. Converting three suppressed-window tests onto it removed what they check:

  • With the window wait removed from the product code, A_Late_Negative… and Suppressed_Send_Windows_Are_Kept_Per_Service stayed green on the converted version, and both go red on main.
  • With the remaining window dropped on cancellation, A_Cancelled_Wait_Keeps_The_Rest_Of_The_Window stayed green, in 3 of 3 runs.

The reason: the window wait is bounded by a real CancellationTokenSource. Whether the late response or the next request's transmit comes first is therefore decided on the wall clock. Once the ECU's delay becomes a virtual advance, the response arrives before that transmit and is discarded as stale, whatever the window does. I'll record the details on #171.

Type of change

  • feat — new behaviour (minor release)
  • fix / perf — bug or performance fix (patch release)
  • docs / test / refactor / chore / ci — no release
  • Breaking change (! in the title, plus a BREAKING CHANGE: footer explaining the migration)

Checklist

  • dotnet build CanKit.Pro.sln -c Release succeeds (with -p:CI=true)
  • dotnet test CanKit.Pro.sln -c Release passes (net10.0, locally)
  • Public API changes are documented with XML comments (none; the new members are internal)
  • New behaviour is covered by a test
  • The requirement or ADR this relates to is referenced (e.g. FR-RAW-031, ADR-7), if any (Replace wall-clock category-2 test sleeps (macOS flake risk) #171)

🤖 Generated with Claude Code

https://claude.ai/code/session_013WJ8h1ahw4Nj5dYuEWy34s


Generated by Claude Code

UdsClientImpl and UdsFunctionalClient take _requestLock through a new
AcquireRequestLockAsync helper. It raises the internal
RequestLockContended event when the lock is already held, and
UdsClientImpl also raises RequestLockAcquired once it is taken. Tests
use them in place of wall-clock sleeps timed to land while another call
holds the lock (#171). Neither event has a subscriber in production.

The zero-timeout probe takes the caller's token, so a call cancelled
before it starts throws rather than taking a free lock, as the plain
WaitAsync it replaces did.

Refs #171

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WJ8h1ahw4Nj5dYuEWy34s
Converts the lock and queue rows of the UDS audit (#171, audit item 7)
from wall-clock sleeps to the request-lock observables:

- UdsClientTests: Dispose_During_InFlight_Request_Does_Not_Race_RequestLock,
  Dispose_Cancels_Suppress_TesterPresent_Blocked_On_RequestLock and
  SecurityAccess_Holds_Lock_Across_Seed_And_Key. The last one now needs
  a keep-alive tick to have actually queued behind SecurityAccess;
  before, it counted 30 ms periods and could pass with no tick at all.
- UdsExpiredDeadlineTests: N_Dispose_Leaves_The_Lock_To_A_Holder_That_Outlasts_The_Wait.
- UdsFunctionalClientTests: A_Call_Queued_Behind_Another_Does_Not_Send_After_Dispose.
- UdsTransferTests: DownloadAsync_Holds_Exclusive_Lock_Against_Concurrent_TesterPresent.
  Both TesterPresent calls must be seen queued on the lock while the
  download holds it, replacing the 2 ms race width. The test asserts
  that this happened.

Adds tests for a pre-cancelled call on a free lock and for a contended
lock with no subscriber.

An_Invalid_Collection_Window_Transmits_Nothing keeps its 50 ms window,
with a comment explaining why: it is a negative check on another bus,
and a regression's frame would reach that bus asynchronously.

Refs #171

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WJ8h1ahw4Nj5dYuEWy34s
@cursor

cursor Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

PR Summary

Low Risk
Internal test hooks and a thin lock-acquisition wrapper; production UDS serialization semantics are unchanged aside from preserving pre-cancel behaviour on the uncontended path.

Overview
Part of #171: UDS tests no longer use fixed Task.Delay to guess when another call has reached _requestLock.

Production (internal only): UdsClientImpl and UdsFunctionalClient route lock acquisition through AcquireRequestLockAsync, which probes with a zero-timeout Wait (uncontended path stays cheap), then raises RequestLockContended when a waiter actually queues and RequestLockAcquired on UdsClientImpl once the lock is held. Subscribers are tests only; behavior matches the previous WaitAsync path, including honouring a token already cancelled before acquisition.

Tests: Lock/queue scenarios (SecurityAccess vs keep-alive, download vs concurrent TesterPresent, dispose races, functional client queue behind an open window) synchronize on those events instead of millisecond sleeps. New coverage includes uncontended contention with no subscriber and no lock taken when cancelled upfront.

Reviewed by Cursor Bugbot for commit 5119a36. Bugbot is set up for automated code reviews on this repo. Configure here.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-27T20:32:01.329691Z 5119a36 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@codecov

codecov Bot commented Sep 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@dborgards
dborgards merged commit a109f4a into main Sep 27, 2026
14 checks passed
@dborgards
dborgards deleted the claude/pensive-wozniak-ivbwba branch September 27, 2026 20:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants